package uk.ac.cam.cl.projectcharlie2010.camera;

import java.awt.BorderLayout;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;

import uk.ac.cam.cl.projectcharlie2010.CharliesConstants;
import uk.ac.cam.cl.projectcharlie2010.clients.desktop.controller.CharlieProgramLogicException;
import uk.ac.cam.cl.projectcharlie2010.game.player.physical.AbortedException;
import uk.ac.cam.cl.projectcharlie2010.game.player.physical.ImpossibleMoveException;
import uk.ac.cam.cl.projectcharlie2010.game.player.physical.VisualMoveRecogniser;
import ch.nostromo.tiffanys.game.move.MoveInput;

public class Camera implements VisualMoveRecogniser{

	private BufferedImage currentImage;
	private URL imageURL;
	private CameraSquare[][] squares = new CameraSquare[8][8];
	private BufferedImage calibrationImage;
	//FIXME norm location
	File calibrationImageLocation = new File(CharliesConstants.SETTINGS_DIR_LOCATION + "/calibrationImage.jpg");
	String thresholdLocation = CharliesConstants.SETTINGS_DIR_LOCATION + "/threshold.javainteger";
	private int[][] changedSquares = new int[6][2];
	private int changedSquaresPointer = 0;
	private int[][] moveStartState = new int[8][8];
	private int[] from = new int[2];
	private int[] to = new int[2];
	private String fromString;
	private String toString;
	private boolean abort = false;
	private int threshold;
	private boolean cameraCalibrated;
	private static Camera instance = null;
	private BufferedImage maskImage = null;

	public static Camera getLiveInstance()  {
		instance = getInstance();
		instance.abort = false;
		return instance;
	}

	public static Camera getInstance() {
		if(instance == null){
			instance = new Camera();
		}
		return instance;
	}

	public void kill(){
		abort = true;
	}

	private Camera() {
		try {
			imageURL = new URL("http://192.168.1.90/jpg/image.jpg");
		} catch (Exception e) {
			e.printStackTrace();
		}
		CameraSquare.setSideLength(60);
		alignSquares();

		try {
			calibrationImage = ImageIO.read(calibrationImageLocation);
			cameraCalibrated = true;
		} catch (IOException e2) {
			cameraCalibrated = false;
		}

		try {
			maskImage = ImageIO.read(new File("resources/camera/mask.png"));
		} catch (IOException e1) {
			System.err.println("No mask image found");
		}

		//LOAD

		try {
			FileInputStream fis = new FileInputStream(new File(thresholdLocation));
			ObjectInputStream ois = new ObjectInputStream(fis);

			threshold = (Integer) ois.readObject();

			System.out.println("Threshold: " + threshold);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();

		}

		for(int i=0;i<=7;i++){
			for(int j=0;j<=7;j++){
				if(j==0||j==1){
					squares[i][j].setCurrentPiece(1);
					squares[i][j].setPreviousPiece(1);
				}else if(j==6||j==7){
					squares[i][j].setCurrentPiece(2);
					squares[i][j].setPreviousPiece(2);
				}else{
					squares[i][j].setCurrentPiece(0);
					squares[i][j].setPreviousPiece(0);
				}
			}
		}
	}

	//Calculate threshold for determining whether squares are empty,
	//and stores the threshold to a file.

	public void calibrate() {

		URL calibrationImageURL;

		try {
			calibrationImageURL = new URL("http://192.168.1.90/jpg/image.jpg");

			BufferedImage comparisonImage = ImageIO.read(calibrationImageURL);

			BufferedImage diffImage = makeSubtractedImage(comparisonImage);

			int emptyMaxVal = 0;
			int fullMinVal = 1000000000; //Start at infinity


			for(int i=0;i<=7;i++){
				for(int j=0;j<=7;j++ ){
					int RGBSum = 0;
					for(int k=squares[i][j].getXcoord();k<squares[i][j].getXcoord()+CameraSquare.getSideLength();k++){
						for(int l=squares[i][j].getYcoord();l<squares[i][j].getYcoord()+CameraSquare.getSideLength();l++){
							if(!isMasked(k,l)){
								int RGB = diffImage.getRGB(k, l);
								int R = getRValue(RGB);
								int G = getGValue(RGB);
								int B = getBValue(RGB);

								RGBSum += R*R+G*G+B*B;
							}
						}
					}

					if (j<2 || 5<j) {
						if (RGBSum < fullMinVal) {
							fullMinVal = RGBSum;
						}
					} else {
						if (RGBSum > emptyMaxVal) {
							emptyMaxVal = RGBSum;
						}
					}

				}
			}

			for(int j=7;j>=0;j--){
				for(int i=0;i<=7;i++){
					int RSum = 0;
					int BSum = 0;
					int GSum = 0;


					for(int k=squares[i][j].getXcoord();k<squares[i][j].getXcoord()+CameraSquare.getSideLength();k++){
						for(int l=squares[i][j].getYcoord();l<squares[i][j].getYcoord()+CameraSquare.getSideLength();l++){
							if(!isMasked(k,l)){
								RSum += getRValue(comparisonImage.getRGB(k, l));
								GSum += getGValue(comparisonImage.getRGB(k, l));
								BSum += getBValue(comparisonImage.getRGB(k, l));

							}
						}
					}
					//					System.out.print("| "+RSum+"; "+GSum+"; "+BSum+"| ");
				}
				//				System.out.println();
			}


			threshold = (fullMinVal + emptyMaxVal) / 2; 

			System.out.println("New threshold: " + threshold);
			System.out.println("EmptyMaxVal: " + emptyMaxVal);
			System.out.println("FullMinVal: " + fullMinVal);

			//STORE
			FileOutputStream fos = null;
			ObjectOutputStream oos = null;

			try {
				fos = new FileOutputStream(new File(thresholdLocation));
				oos = new ObjectOutputStream(fos);
				oos.writeObject(threshold); //TODO correct object;
				oos.close();
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		} catch (MalformedURLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}


	//Takes an image from the camera with empty board for background subtraction, 
	//and stores it to disk.
	public void updateBackgroundCeiling() {
		URL calibrationImageURL;
		try {
			calibrationImageURL = new URL("http://192.168.1.90/jpg/image.jpg");
			calibrationImage = ImageIO.read(calibrationImageURL);

			cameraCalibrated = true;

			ImageIO.write(calibrationImage, "jpg", calibrationImageLocation);

		}catch(Exception e){
			e.printStackTrace();
		}
	}

	//Initialise a CameraSquare with the given x and y coordinate.
	public void setSquare(int x, int y, CameraSquare cs){
		squares[x][y] = cs;
	}

	private void alignSquares(){
		CameraAlignment.align(this);
	}

	//See VisualMoveRecogniser Interface.
	public boolean isCalibrated() {
		return cameraCalibrated;
	}

	//See VisualMoveRecogniser Interface.
	public MoveInput seeNextMove() throws AbortedException, ImpossibleMoveException{
		refresh();

		showAlignmentImage();
		for(int i=0;i<=7;i++){
			for(int j=0;j<=7;j++){
				moveStartState[i][j] = squares[i][j].getCurrentPiece();
			}
		}

		printBoardEmptyVals();
		printBoard();

		while(changedSquaresPointer == 0){
			detectMove();
		}

		decodeMove();

		decodeStrings();



		if(abort){
			throw new AbortedException();
		}
		System.out.println("Left Castle.");
		System.out.println("Left Castle.");
		System.out.println("Left Castle.");

		System.out.println("From: " + fromString);
		System.out.println("To: " + toString);

		return new MoveInput(fromString,toString);
	}

	private void detectMove() throws AbortedException, ImpossibleMoveException{
		while(changedSquaresPointer == 0){
			if (abort) {
				throw new AbortedException();
			}
			refresh();

		}

		//Countdown timer that ends 3 seconds after last move is detected
		Date timeOfLastMove = new Date();
		Date currentTime = new Date();
		while (currentTime.getTime() - timeOfLastMove.getTime() < 3000) {
			if (abort) {
				throw new AbortedException();
			}
			currentTime = new Date();
			refresh();
			if (changedSquaresPointer != 0) {
				timeOfLastMove = new Date();
			}
		}

		for(int i=0;i<=7;i++){
			for(int j=0;j<=7;j++){
				squares[i][j].setCurrentPiece(moveStartState[i][j]);
			}
		}

		refresh();
		printBoard();
	}

	private void refresh() throws ImpossibleMoveException{
		try{
			currentImage = ImageIO.read(imageURL);

		}catch(IOException e){
			e.printStackTrace();
		}
		for(int i=0; i<=3;i++){
			changedSquares[i][0] = 10;
			changedSquares[i][1] = 10;
		}

		BufferedImage subtractedImage = makeSubtractedImage(currentImage);
		changedSquaresPointer = 0;
		try{
			for(int i=0;i<=7;i++){
				for(int j=0;j<=7;j++ ){
					int currentPiece = decodePiece(i,j,subtractedImage);
					if(squares[i][j].updatePiece(currentPiece)){ //NOTE: HAS SIDE EFFECTS!
						changedSquares[changedSquaresPointer][0] = i;
						changedSquares[changedSquaresPointer][1] = j;
						if (changedSquaresPointer != 5) {
							changedSquaresPointer ++;		
						}
						//						System.out.println("CSP: "+changedSquaresPointer);
						//						System.out.println(""+i+ ", " + j);
					}
				}
				System.out.println();
			}
		}catch(ArrayIndexOutOfBoundsException e){
			System.err.println("AIOOB exception thrown in refresh");
			e.printStackTrace();
			throw new ImpossibleMoveException();
		}
	}

	private BufferedImage makeSubtractedImage(BufferedImage currentImage) {

		if (calibrationImage == null) {
			throw new CharlieProgramLogicException("Camera not calibrated");
		}

		BufferedImage subtractedImage = new BufferedImage(calibrationImage.getWidth(), calibrationImage.getHeight(), calibrationImage.getType());
		for(int i=0;i<calibrationImage.getWidth();i++){
			for(int j=0;j<calibrationImage.getHeight();j++ ){
				if(!isMasked(i,j)){
					int currentRGB = currentImage.getRGB(i, j);
					int initialRGB = calibrationImage.getRGB(i, j);
					int currentR = getRValue(currentRGB);
					int currentG = getGValue(currentRGB);
					int currentB = getBValue(currentRGB);
					int initialR = getRValue(initialRGB);
					int initialG = getGValue(initialRGB);
					int initialB = getBValue(initialRGB);
					int Rdiff = Math.abs(currentR - initialR);
					int Gdiff = Math.abs(currentG - initialG);
					int Bdiff = Math.abs(currentB - initialB);
					int subtractedRGB = compactRGB(Rdiff, Gdiff, Bdiff);
					subtractedImage.setRGB(i, j, subtractedRGB);
				}else{
					subtractedImage.setRGB(i,j,0);
				}
			}
		}
		//		displayImage(subtractedImage);
		//		try {
		//			ImageIO.write(subtractedImage, "jpg", new File("C:\\subtractedImage.jpg"));
		//			//TODO RAT;
		//		} catch (IOException e) {
		//			// TODO Auto-generated catch block
		//			e.printStackTrace();
		//		}
		//		displayImage(subtractedImage);
		return subtractedImage;

	}

	private int decodePiece (int x, int y, BufferedImage subtractedImage) {
		if (emptySquare(x,y, subtractedImage) < threshold) {
			return 0;
		} 
		else return P1orP2(x,y,subtractedImage);

	}

	private boolean isMasked(int x, int y){
		if(maskImage.getRGB(x,y) == 0xFFFFFFFF){
			return false;
		}
		return true;
	}

	private int emptySquare(int x, int y, BufferedImage subtractedImage) {


		CameraSquare cs = squares[x][y]; 
		int RGBTotal = 0;
		for(int i=cs.getXcoord();i<cs.getXcoord()+CameraSquare.getSideLength();i++){
			for(int j=cs.getYcoord();j<cs.getYcoord()+CameraSquare.getSideLength();j++ ){
				if(!isMasked(i,j)){
					int RGB = subtractedImage.getRGB(i, j);
					int R = getRValue(RGB);
					int G = getGValue(RGB);
					int B = getBValue(RGB);
					RGBTotal += (R*R + G*G + B*B);

					//TODO confirm change working
				}
			}
		}
		return RGBTotal;
	}

	private int P1orP2(int x, int y, BufferedImage subtractedImage) {
		//		int P1count = 0;
		//		int P2count = 0;
		int rSum = 0;
		int gSum = 0;
		int bSum = 0;
		CameraSquare cs = squares[x][y];
		for(int i=cs.getXcoord();i<cs.getXcoord()+CameraSquare.getSideLength();i++){
			for(int j=cs.getYcoord();j<cs.getYcoord()+CameraSquare.getSideLength();j++ ){
				if(!isMasked(i,j)){
					int subRGB = subtractedImage.getRGB(i, j);
					int subR = getRValue(subRGB);
					int subG = getGValue(subRGB);
					int subB = getBValue(subRGB);
					int pixelRValue = getRValue(currentImage.getRGB(i, j));
					int pixelGValue = getGValue(currentImage.getRGB(i, j));
					int pixelBValue = getBValue(currentImage.getRGB(i, j));
					if ((subR + subG + subB)>50){
						rSum += pixelRValue*pixelRValue;
						gSum += pixelBValue*pixelBValue;
						bSum += pixelBValue*pixelBValue;
					}


					//					if ((pixelRValue) > (pixelBValue)) {	//Testing which piece it is
					//						P1count++;							
					//					}
					//					else P2count++;
					//					}
				}
			}
		}
		System.out.print("/"+(rSum+gSum+bSum)+"/");
		return (rSum>bSum)?1:2;
	}

	private void decodeMove() throws ImpossibleMoveException{
		int[][] pieceChanges = new int[6][2];	
		for(int i=0;i<changedSquaresPointer;i++){	
			try {
				pieceChanges[i][0] = squares[changedSquares[i][0]][changedSquares[i][1]].getPreviousPiece();
				pieceChanges[i][1] = squares[changedSquares[i][0]][changedSquares[i][1]].getCurrentPiece();
			}catch(ArrayIndexOutOfBoundsException e){
				System.err.println("AIOOB exception thrown in decodeMove");
				e.printStackTrace();
				throw new ImpossibleMoveException();
			}
		}


		if(changedSquaresPointer == 4){
			boolean kingside = false;

			from[0] = 4;
			from[1] = changedSquares[0][1];
			to[1] = changedSquares[0][1];	
			for(int i = 0; i<changedSquaresPointer; i++){
				if(changedSquares[i][1] != from[1]){
					throw new ImpossibleMoveException();
				}
			}

			for(int i = 0; i<changedSquaresPointer; i++){
				if(changedSquares[i][0] == 7){
					kingside = true;
					to[0] = 6;
				}else if(changedSquares[i][0] == 0){
					kingside = false;
					to[0] = 2;
				}
			}

			if(kingside){
				//				System.out.println("Kingside castle.");
			}else{
				//				System.out.println("Queenside castle.");
			}
		}else if(changedSquaresPointer == 3){

			//			System.out.println("En Passant");
			for (int i = 0; i<changedSquaresPointer; i++) {
				if (pieceChanges[i][0] == 0) {
					to[0] = changedSquares[i][0];
					to[1] = changedSquares[i][1];
					for (int k = 0; k<changedSquaresPointer; k++) {
						if (changedSquares[k][0] != to[0]) { //Note: to[0] is not guaranteed to be correctly initialized (is it?)
							from[0] = changedSquares[k][0];
							from[1] = changedSquares[k][1];
							return;
						}
					}
				}
			}
		}else if(changedSquaresPointer == 2){
			if(pieceChanges[0][1] == 0){
				from[0] = changedSquares[0][0];
				from[1] = changedSquares[0][1];
				to[0] = changedSquares[1][0];
				to[1] = changedSquares[1][1];
			}else{
				from[0] = changedSquares[1][0];
				from[1] = changedSquares[1][1];
				to[0] = changedSquares[0][0];
				to[1] = changedSquares[0][1];
			}
		}else {			
			System.err.println("Invalid number of changed squares! "+ changedSquaresPointer);
			throw new ImpossibleMoveException(); 
		}

	}

	private void decodeStrings(){
		fromString = numToLetter(from[0])+Integer.toString(from[1]+1);
		toString = numToLetter(to[0])+Integer.toString(to[1]+1);
	}

	private String numToLetter(int num){
		String s = null;
		switch(num){
		case 0: s = "A"; break;
		case 1: s = "B"; break;
		case 2: s = "C"; break;
		case 3: s = "D"; break;
		case 4: s = "E"; break;
		case 5: s = "F"; break;
		case 6: s = "G"; break;
		case 7: s = "H"; break;
		}
		return s;
	}

	private int getRValue(int rgb){
		return (16711680&rgb)>>16;
	}

	private int getGValue(int rgb){
		return (65280&rgb)>>8;
	}

	private int getBValue(int rgb){
		return 255&rgb;
	}

	private int compactRGB (int R, int G, int B){
		int RGB = B + (G<<8) + (R<<16);
		return RGB;
	}

	private void printBoard(){
		for(int i=7;i>=0;i--){
			for(int j=0;j<=7;j++){
				System.out.print(squares[j][i].getCurrentPiece());

			}
			System.out.print("\n");
		}
		System.out.println(" ");
	}

	private void showAlignmentImage(){
		BufferedImage alignmentImage = new BufferedImage(currentImage.getWidth(),currentImage.getHeight(),currentImage.getType());
		for(int i=0;i<currentImage.getHeight();i++){
			for(int j=0;j<currentImage.getWidth();j++){
				//				if (!isMasked(j, i)) {
				alignmentImage.setRGB(j,i,currentImage.getRGB(j,i));
				//				}
			}
		}
		for(int i=0;i<8;i++){
			for(CameraSquare cs :squares[i]){
				for(int k = cs.getXcoord();k < cs.getXcoord()+CameraSquare.getSideLength();k++){
					for(int l= cs.getYcoord();l<cs.getYcoord()+CameraSquare.getSideLength();l++){
						if (!isMasked(k,l)) 
							alignmentImage.setRGB(k,l,0xB0171F);
					}

				}
			}
		}
		displayImage(alignmentImage);
	}

	private void displayImage(BufferedImage image){
		JFrame frame = new JFrame(); 
		JLabel label = new JLabel(new ImageIcon(image)); 
		frame.getContentPane().add(label, BorderLayout.CENTER); 
		frame.pack(); frame.setVisible(true);
	}

	private void printBoardEmptyVals() {
		BufferedImage subtractedImage = makeSubtractedImage(currentImage);
		for(int i=7;i>=0;i--){
			for(int j=0;j<=7;j++){
				System.out.print("|" + emptySquare(j,i,subtractedImage) + "|");

			}
			System.out.print("\n");
		}
		System.out.println(" ");
	}

}